// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:kernel/ast.dart' as ast show DartType, Name;
import 'package:cfg/ir/constant_value.dart';
import 'package:cfg/ir/flow_graph.dart';
import 'package:cfg/ir/functions.dart';
import 'package:cfg/ir/local_variable.dart';
import 'package:cfg/ir/loops.dart';
import 'package:cfg/ir/source_position.dart';
import 'package:cfg/ir/types.dart';
import 'package:cfg/ir/use_lists.dart';
import 'package:cfg/ir/visitor.dart';
import 'package:cfg/utils/misc.dart';

/// Base class for all instructions.
abstract base class Instruction {
  /// Enclosing control-flow graph.
  final FlowGraph graph;

  /// Index in the [FlowGraph.instructions].
  final int id;

  /// Source position associated with this instruction.
  final SourcePosition sourcePosition;

  /// Explicit inputs.
  final UsesArray _inputs;

  /// Enclosing basic block.
  Block? block;

  /// Next instruction in basic block.
  Instruction? next;

  /// Previous instruction in basic block.
  Instruction? previous;

  /// Values implicitly used by this instruction
  /// e.g. for exception handling and deoptimization).
  UsesArray? implicitInputs;

  /// Create a new instruction.
  ///
  /// The newly created instruction belongs to [graph] but not linked into
  /// a basic block and uses lists.
  Instruction(this.graph, this.sourcePosition, int inputCount)
    : id = graph.instructions.length,
      _inputs = inputCount == 0
          ? graph.emptyUsesArray
          : UsesArray.allocate(graph, inputCount) {
    for (var i = 0; i < inputCount; ++i) {
      _inputs.at(graph, i).init(graph, this);
    }
    graph.instructions.add(this);
  }

  /// Returns true if this instruction is linked into a list of instructions
  /// in a basic block (also imlplies inputs are linked in their use lists).
  bool get isInGraph => block != null;

  /// Number of explicit inputs.
  int get inputCount => _inputs.getLength(graph);

  /// [Use] corresponding to an [i]-th explicit input.
  Use inputAt(int i) => _inputs.at(graph, i);

  /// [Definition] of [i]-th explicit input.
  Definition inputDefAt(int i) => inputAt(i).getDefinition(graph);

  /// Set [i]-th input to the given [value]. Can be done only once.
  void setInputAt(int i, Definition value) {
    final input = inputAt(i);
    assert(input.getNext(graph) == Use.Null);
    assert(input.getPrevious(graph) == Use.Null);
    input.setDefinition(graph, value);
  }

  /// Replace [i]-th input with [value]. This instruction should be
  /// linked into a basic block.
  void replaceInputAt(int i, Definition value) {
    assert(isInGraph);
    removeInputFromUseList(i);
    setInputAt(i, value);
    addInputToUseList(i);
  }

  /// Reduce number of inputs to [newInputCount].
  void truncateInputs(int newInputCount) {
    _inputs.truncateTo(graph, newInputCount);
  }

  /// Link this instruction to the [next] instruction in basic block.
  void linkTo(Instruction next) {
    assert(!identical(this, next));
    this.next = next;
    next.previous = this;
    next.block = this.block;
  }

  /// Add [inputIndex]-th input to use list of its definition.
  ///
  /// This is a low-level operation which is rarely needed.
  /// Prefer using [appendInstruction] or [insertAfter].
  void addInputToUseList(int inputIndex) {
    final input = inputAt(inputIndex);
    assert(input.getNext(graph) == Use.Null);
    assert(input.getPrevious(graph) == Use.Null);
    final def = input.getDefinition(graph);
    final nextUse = def._inputUses;
    if (nextUse != Use.Null) {
      assert(nextUse.getPrevious(graph) == Use.Null);
      input.setNext(graph, nextUse);
      nextUse.setPrevious(graph, input);
    }
    def._inputUses = input;
  }

  void _addInputsToUseLists() {
    for (int i = 0, n = inputCount; i < n; ++i) {
      addInputToUseList(i);
    }
    assert(implicitInputs == null);
  }

  /// Remove [inputIndex]-th input from use list of its definition.
  ///
  /// This is a low-level operation which is rarely needed.
  /// Prefer using [replaceInputAt] or [removeFromGraph].
  void removeInputFromUseList(int inputIndex) {
    final input = inputAt(inputIndex);
    assert(input.getInstruction(graph) == this);
    final nextUse = input.getNext(graph);
    final prevUse = input.getPrevious(graph);
    if (prevUse == Use.Null) {
      final def = input.getDefinition(graph);
      assert(def._inputUses == input);
      def._inputUses = nextUse;
    } else {
      prevUse.setNext(graph, nextUse);
    }
    if (nextUse != Use.Null) {
      nextUse.setPrevious(graph, prevUse);
    }
    input.setNext(graph, Use.Null);
    input.setPrevious(graph, Use.Null);
  }

  /// Remove all inputs from their use lists.
  void removeInputsFromUseLists() {
    for (int i = 0, n = inputCount; i < n; ++i) {
      removeInputFromUseList(i);
    }
  }

  /// Append an unlinked [instr] after this instruction,
  /// which should be the last instruction appended into basic block.
  ///
  /// Also, add all inputs of [instr] to their use lists.
  void appendInstruction(Instruction instr) {
    assert(isInGraph);
    assert(!instr.isInGraph);
    assert(this.next == null);
    assert(this.block!.lastInstruction == this);
    assert(instr.next == null);
    assert(instr.previous == null);
    assert(instr.block == null);
    linkTo(instr);
    instr._addInputsToUseLists();
    block!.lastInstruction = instr;
  }

  /// Insert this instruction between [previous] and [previous.next].
  /// (which should be linked into a basic block).
  ///
  /// If [addInputsToUseLists], then also add all inputs to their use lists.
  void insertAfter(Instruction previous, {bool addInputsToUseLists = true}) {
    assert(previous.isInGraph);
    assert(!isInGraph);
    final next = previous.next!;
    assert(previous.block != null);
    assert(this.next == null);
    assert(this.previous == null);
    assert(this.block == null);

    this.previous = previous;
    this.next = next;
    this.block = previous.block;
    next.previous = this;
    previous.next = this;

    if (addInputsToUseLists) {
      _addInputsToUseLists();
    }
  }

  /// Insert this instruction between [next.previous] and [next].
  /// (which should be linked into a basic block).
  ///
  /// If [addInputsToUseLists], then also add all inputs to their use lists.
  void insertBefore(Instruction next, {bool addInputsToUseLists = true}) {
    assert(next.isInGraph);
    insertAfter(next.previous!, addInputsToUseLists: addInputsToUseLists);
  }

  /// Unlink this instruction from its basic block and use lists.
  void removeFromGraph() {
    assert(isInGraph);
    assert(block != null);
    assert(this != block);

    removeInputsFromUseLists();
    final next = this.next;
    final previous = this.previous!;
    previous.next = next;
    if (next != null) {
      next.previous = previous;
    } else {
      assert(this is ControlFlowInstruction);
      assert(this == block!.lastInstruction);
      block!.lastInstruction = previous;
    }
    this.next = null;
    this.previous = null;
    this.block = null;
    assert(!isInGraph);
  }

  /// Whether this instruction can potentially throw a Dart exception.
  bool get canThrow;

  /// Whether this instruction can have any visible side-effects.
  bool get hasSideEffects;

  /// Returns true if this instruction is idempotent (i.e. repeating this
  /// instruction with the same inputs does not have any effects after
  /// executing it once), and it is a subject to value numbering.
  bool get isIdempotent => false;

  /// Returns true if extra instruction attributes are equal.
  /// Used only for idempotent instructions of the same type.
  bool attributesEqual(Instruction other) =>
      throw 'Not implemented for ${runtimeType}';

  /// Return true if [other] dominates this instruction, i.e. every path from
  /// graph entry to this instruction goes through [other].
  bool isDominatedBy(Instruction other) =>
      graph.dominators.isDominatedBy(this, other);

  R accept<R>(InstructionVisitor<R> v);
}

/// Trait of instructions which can potentially throw Dart exceptions.
base mixin CanThrow on Instruction {
  bool get canThrow => true;
}

/// Trait of instructions which cannot throw Dart exceptions.
base mixin NoThrow on Instruction {
  bool get canThrow => false;
}

/// Trait of instructions which can have visible side-effects.
base mixin HasSideEffects on Instruction {
  bool get hasSideEffects => true;
}

/// Trait of instructions which do not have any side-effects.
base mixin Pure on Instruction {
  bool get hasSideEffects => false;
}

/// Trait of idempotent instructions.
base mixin Idempotent on Instruction {
  bool get isIdempotent => true;
}

/// Base class for instructions which yield a value.
abstract base class Definition extends Instruction {
  Use _inputUses = Use.Null;
  Use _implicitUses = Use.Null;

  Definition(super.graph, super.sourcePosition, super.inputCount);

  UsesIterable get inputUses => UsesIterable(graph, _inputUses);
  UsesIterable get implicitUses => UsesIterable(graph, _implicitUses);

  bool get hasInputUses => _inputUses != Use.Null;
  bool get hasImplicitUses => _implicitUses != Use.Null;
  bool get hasUses => hasInputUses || hasImplicitUses;

  /// Replace all uses of this instruction with [other].
  void replaceUsesWith(Definition other) {
    if (hasInputUses) {
      Use last = Use.Null;
      for (final use in inputUses) {
        use.setDefinition(graph, other);
        last = use;
      }
      final tail = other._inputUses;
      last.setNext(graph, tail);
      if (tail != Use.Null) {
        tail.setPrevious(graph, last);
      }
      other._inputUses = this._inputUses;
      this._inputUses = Use.Null;
    }
    if (hasImplicitUses) {
      Use last = Use.Null;
      for (final use in implicitUses) {
        use.setDefinition(graph, other);
        last = use;
      }
      final tail = other._implicitUses;
      last.setNext(graph, tail);
      if (tail != Use.Null) {
        tail.setPrevious(graph, last);
      }
      other._implicitUses = this._implicitUses;
      this._implicitUses = Use.Null;
    }
  }

  /// Result type of this instruction.
  CType get type;

  /// Whether this instruction can yield a zero value.
  bool get canBeZero => true;

  /// Whether this instruction can yield a negative value.
  bool get canBeNegative => true;

  /// Returns the only instruction which uses result of this instruction as
  /// an explicit input.
  ///
  /// Returns `null` if there are no or multiple uses.
  Instruction? get singleUser {
    if (hasInputUses && !hasImplicitUses) {
      final use = _inputUses;
      if (use.getNext(graph) == Use.Null) {
        return use.getInstruction(graph);
      }
    }
    return null;
  }
}

/// Forward iterator over instructions in the block.
final class _InstructionsIterator implements Iterator<Instruction> {
  Instruction? _current;
  Instruction? _next;

  _InstructionsIterator(this._next);

  @override
  bool moveNext() {
    _current = _next;
    _next = _current?.next;
    return (_current != null);
  }

  @override
  Instruction get current => _current!;
}

/// Backwards iterator over instructions in the block.
final class _ReverseInstructionsIterator implements Iterator<Instruction> {
  Instruction? _current;
  Instruction _previous;

  _ReverseInstructionsIterator(this._previous);

  @override
  bool moveNext() {
    final instr = _previous.previous;
    if (instr == null) {
      assert(_previous is Block);
      _current = null;
      return false;
    } else {
      _current = _previous;
      _previous = instr;
      return true;
    }
  }

  @override
  Instruction get current => _current!;
}

/// Iterable for backwards iteration of instructions in the block.
final class _ReverseInstructionsIterable extends Iterable<Instruction> {
  final Block _block;

  _ReverseInstructionsIterable(this._block);

  @override
  Iterator<Instruction> get iterator =>
      _ReverseInstructionsIterator(_block.lastInstruction);
}

/// Basic block.
abstract base class Block extends Instruction
    with NoThrow, Pure, Iterable<Instruction> {
  final List<Block> predecessors = [];
  final List<Block> successors = [];
  late Instruction lastInstruction;
  CatchBlock? exceptionHandler;

  int preorderNumber = -1;
  int postorderNumber = -1;

  Block(FlowGraph graph, SourcePosition sourcePosition)
    : super(graph, sourcePosition, 0) {
    this.block = this;
    this.lastInstruction = this;
  }

  Block? get dominator {
    int idom = graph.dominators.idom[preorderNumber];
    return (idom < 0) ? null : graph.preorder[idom];
  }

  List<Block> get dominatedBlocks => graph.dominators.dominated[preorderNumber];

  Loop? get loop => graph.loops[this];

  int get loopDepth => loop?.depth ?? 0;

  /// Replace [oldPredecessor] with [newPredecessor].
  void replacePredecessor(Block oldPredecessor, Block newPredecessor) {
    final predecessors = this.predecessors;
    for (int i = 0, n = predecessors.length; i < n; ++i) {
      if (predecessors[i] == oldPredecessor) {
        predecessors[i] = newPredecessor;
      }
    }
  }

  /// Iterate over instructions in the block from the first to the last.
  /// Iteration is robust wrt removal of the current instruction.
  @override
  Iterator<Instruction> get iterator => _InstructionsIterator(next);

  /// Iterate over instructions in the block from the last to the first.
  /// Iteration is robust wrt removal of the current instruction.
  Iterable<Instruction> get reversed => _ReverseInstructionsIterable(this);
}

/// The entry block in the [FlowGraph].
///
/// Contains all [Constant] instructions and [Parameter] instructions
/// corresponding to the function parameters.
final class EntryBlock extends Block {
  EntryBlock(super.graph, super.sourcePosition);

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitEntryBlock(this);
}

/// Iterator over [Phi] instructions in the [JoinBlock].
final class _PhiIterator implements Iterator<Phi> {
  Phi? _current;
  Phi? _next;

  _PhiIterator(JoinBlock block) {
    final nextInstruction = block.next;
    _next = (nextInstruction is Phi) ? nextInstruction : null;
  }

  @override
  bool moveNext() {
    _current = _next;
    final nextInstruction = _current?.next;
    _next = (nextInstruction is Phi) ? nextInstruction : null;
    return (_current != null);
  }

  @override
  Phi get current => _current!;
}

/// Iterable over [Phi] instructions in the [JoinBlock].
final class _PhiIterable extends Iterable<Phi> {
  final JoinBlock _block;

  _PhiIterable(this._block);

  @override
  Iterator<Phi> get iterator => _PhiIterator(_block);
}

/// Basic block which can have multiple predecessors.
///
/// May contain [Phi] instructions in the beginning of the block.
final class JoinBlock extends Block {
  JoinBlock(super.graph, super.sourcePosition);

  Iterable<Phi> get phis => _PhiIterable(this);

  bool get hasPhis => next is Phi;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitJoinBlock(this);
}

/// Target block of a branch.
final class TargetBlock extends Block {
  TargetBlock(super.graph, super.sourcePosition);

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitTargetBlock(this);
}

/// Exception handler block.
///
/// May contain [Parameter] instructions in the beginning of the block
/// to represent incoming values of local variables, exception object and
/// stack trace.
final class CatchBlock extends Block {
  CatchBlock(super.graph, super.sourcePosition);

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitCatchBlock(this);
}

/// Marker interface for instructions which can end basic block.
abstract class ControlFlowInstruction {}

/// Unconditional jump to the sole successor of this basic block.
final class Goto extends Instruction
    with NoThrow, Pure
    implements ControlFlowInstruction {
  Goto(FlowGraph graph, SourcePosition sourcePosition)
    : super(graph, sourcePosition, 0);

  Block get target => block!.successors.single;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitGoto(this);
}

/// Conditional branch to either true of false successors of this basic block.
final class Branch extends Instruction
    with NoThrow, Pure
    implements ControlFlowInstruction {
  Branch(FlowGraph graph, SourcePosition sourcePosition, Definition condition)
    : super(graph, sourcePosition, 1) {
    setInputAt(0, condition);
  }

  Definition get condition => inputDefAt(0);
  TargetBlock get trueSuccessor => block!.successors[0] as TargetBlock;
  TargetBlock get falseSuccessor => block!.successors[1] as TargetBlock;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitBranch(this);
}

/// Unconditional jump to the try block body (the first succesor of this
/// basic block). The second successor of this basic block is a catch block.
final class TryEntry extends Instruction
    with NoThrow, Pure
    implements ControlFlowInstruction {
  TryEntry(FlowGraph graph, SourcePosition sourcePosition)
    : super(graph, sourcePosition, 0);

  TargetBlock get tryBody => block!.successors[0] as TargetBlock;
  CatchBlock get catchBlock => block!.successors[1] as CatchBlock;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitTryEntry(this);
}

/// The result of instruction `v = Phi(v[0],...,v[N-1])` is `v[i]` when
/// control is transferred from the i-th predecessor block.
///
/// Number of inputs always matches number of predecessor blocks.
final class Phi extends Definition with NoThrow, Pure {
  /// Local variable for which this [Phi] was originally created.
  final LocalVariable variable;

  Phi(super.graph, super.sourcePosition, this.variable, super.inputCount);

  @override
  CType get type => variable.type;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitPhi(this);
}

/// Return value from the current function.
final class Return extends Instruction
    with NoThrow, Pure
    implements ControlFlowInstruction {
  Return(FlowGraph graph, SourcePosition sourcePosition, Definition value)
    : super(graph, sourcePosition, 1) {
    setInputAt(0, value);
  }

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitReturn(this);
}

enum ComparisonOpcode {
  // Simple object pointer equality.
  equal('=='),
  notEqual('!='),
  // identical with special cases for num.
  identical('==='),
  notIdentical('!=='),
  // int comparisons.
  intEqual('int =='),
  intNotEqual('int !='),
  intLess('int <'),
  intLessOrEqual('int <='),
  intGreater('int >'),
  intGreaterOrEqual('int >='),
  // int bitwise tests
  intTestIsZero('int & == 0'),
  intTestIsNotZero('int & != 0'),
  // double comparisons.
  doubleEqual('double =='),
  doubleNotEqual('double !='),
  doubleLess('double <'),
  doubleLessOrEqual('double <='),
  doubleGreater('double >'),
  doubleGreaterOrEqual('double >=');

  final String token;
  const ComparisonOpcode(this.token);

  bool get isIntComparison => switch (this) {
    intEqual ||
    intNotEqual ||
    intLess ||
    intLessOrEqual ||
    intGreater ||
    intGreaterOrEqual ||
    intTestIsZero ||
    intTestIsNotZero => true,
    _ => false,
  };

  bool get isDoubleComparison => switch (this) {
    doubleEqual ||
    doubleNotEqual ||
    doubleLess ||
    doubleLessOrEqual ||
    doubleGreater ||
    doubleGreaterOrEqual => true,
    _ => false,
  };

  ComparisonOpcode flipOperands() => switch (this) {
    equal ||
    notEqual ||
    identical ||
    notIdentical ||
    intEqual ||
    intNotEqual ||
    intTestIsZero ||
    intTestIsNotZero ||
    doubleEqual ||
    doubleNotEqual => this,
    intLess => intGreaterOrEqual,
    intLessOrEqual => intGreater,
    intGreater => intLessOrEqual,
    intGreaterOrEqual => intLess,
    doubleLess => doubleGreaterOrEqual,
    doubleLessOrEqual => doubleGreater,
    doubleGreater => doubleLessOrEqual,
    doubleGreaterOrEqual => doubleLess,
  };

  bool get canBeNegated => switch (this) {
    doubleLess ||
    doubleLessOrEqual ||
    doubleGreater ||
    doubleGreaterOrEqual => false,
    _ => true,
  };

  ComparisonOpcode negate() => switch (this) {
    equal => notEqual,
    notEqual => equal,
    identical => notIdentical,
    notIdentical => identical,
    intEqual => intNotEqual,
    intNotEqual => intEqual,
    intLess => intGreaterOrEqual,
    intLessOrEqual => intGreater,
    intGreater => intLessOrEqual,
    intGreaterOrEqual => intLess,
    intTestIsZero => intTestIsNotZero,
    intTestIsNotZero => intTestIsZero,
    doubleEqual => doubleNotEqual,
    doubleNotEqual => doubleEqual,
    doubleLess ||
    doubleLessOrEqual ||
    doubleGreater ||
    doubleGreaterOrEqual => throw '${token} cannot be negated',
  };
}

/// Compare two operands.
final class Comparison extends Definition with NoThrow, Pure, Idempotent {
  ComparisonOpcode op;

  Comparison(
    FlowGraph graph,
    SourcePosition sourcePosition,
    this.op,
    Definition left,
    Definition right,
  ) : super(graph, sourcePosition, 2) {
    setInputAt(0, left);
    setInputAt(1, right);
  }

  Definition get left => inputDefAt(0);
  Definition get right => inputDefAt(1);

  @override
  CType get type => const BoolType();

  @override
  bool attributesEqual(covariant Comparison other) => op == other.op;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitComparison(this);
}

/// Instruction representing a constant value.
///
/// Should not be created directly, use [FlowGraph.getConstant] instead.
final class Constant extends Definition with NoThrow, Pure {
  final ConstantValue value;

  Constant(FlowGraph graph, this.value) : super(graph, noPosition, 0);

  @override
  bool get canBeZero => value.isZero;

  @override
  bool get canBeNegative => value.isNegative;

  @override
  CType get type => value.type;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitConstant(this);
}

/// Base class for various calls.
abstract base class CallInstruction extends Definition
    with CanThrow, HasSideEffects {
  CallInstruction(super.graph, super.sourcePosition, super.inputCount);

  bool get hasTypeArguments => inputCount > 0 && inputDefAt(0) is TypeArguments;
  TypeArguments? get typeArguments =>
      hasTypeArguments ? inputDefAt(0) as TypeArguments : null;
}

/// Direct call of the target function.
final class DirectCall extends CallInstruction {
  final CFunction target;

  @override
  final CType type;

  DirectCall(
    super.graph,
    super.sourcePosition,
    this.target,
    super.inputCount,
    this.type,
  );

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitDirectCall(this);
}

/// Interface call via given interface target.
final class InterfaceCall extends CallInstruction {
  final CFunction interfaceTarget;

  @override
  final CType type;

  InterfaceCall(
    super.graph,
    super.sourcePosition,
    this.interfaceTarget,
    super.inputCount,
    this.type,
  );

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitInterfaceCall(this);
}

enum DynamicCallKind { method, getter, setter }

/// Dynamic call via given selector.
final class DynamicCall extends CallInstruction {
  final ast.Name selector;
  final DynamicCallKind kind;

  DynamicCall(
    super.graph,
    super.sourcePosition,
    this.selector,
    this.kind,
    super.inputCount,
  );

  @override
  CType get type => const TopType();

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitDynamicCall(this);
}

/// Parameter of a function or a catch block.
final class Parameter extends Definition with NoThrow, Pure {
  final LocalVariable variable;

  Parameter(FlowGraph graph, SourcePosition sourcePosition, this.variable)
    : super(graph, sourcePosition, 0);

  bool get isFunctionParameter => block is EntryBlock;
  bool get isCatchParameter => block is CatchBlock;

  @override
  CType get type => variable.type;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitParameter(this);
}

/// Loads value from the local variable.
///
/// [LoadLocal] instructions are only used before
/// IR is converted to SSA form.
final class LoadLocal extends Definition with NoThrow, Pure {
  final LocalVariable variable;

  LoadLocal(FlowGraph graph, SourcePosition sourcePosition, this.variable)
    : super(graph, sourcePosition, 0);

  @override
  CType get type => variable.type;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitLoadLocal(this);
}

/// Store value to the local variable.
///
/// [StoreLocal] instructions are only used before
/// IR is converted to SSA form.
final class StoreLocal extends Instruction with NoThrow, HasSideEffects {
  final LocalVariable variable;

  StoreLocal(
    FlowGraph graph,
    SourcePosition sourcePosition,
    this.variable,
    Definition value,
  ) : super(graph, sourcePosition, 1) {
    setInputAt(0, value);
  }

  Definition get value => inputDefAt(0);

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitStoreLocal(this);
}

/// Throw given exception object. Also takes optional stack trace
/// input to rethrow exception object without collecting a new stack trace.
final class Throw extends Instruction
    with CanThrow, HasSideEffects
    implements ControlFlowInstruction {
  Throw(
    FlowGraph graph,
    SourcePosition sourcePosition,
    Definition exception,
    Definition? stackTrace,
  ) : super(graph, sourcePosition, stackTrace != null ? 2 : 1) {
    setInputAt(0, exception);
    if (stackTrace != null) {
      setInputAt(1, stackTrace);
    }
  }

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitThrow(this);
}

/// Represents collection of class and function type parameters.
final class TypeParameters extends Definition with NoThrow, Pure {
  TypeParameters(
    FlowGraph graph,
    SourcePosition sourcePosition,
    Definition? receiver,
  ) : super(graph, sourcePosition, receiver != null ? 1 : 0) {
    if (receiver != null) {
      setInputAt(0, receiver);
    }
  }

  @override
  CType get type => const TypeParametersType();

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitTypeParameters(this);
}

/// Casts input object to the given type.
///
/// Checked casts throw TypeError if
/// object is not assignable to the given type.
final class TypeCast extends Definition with CanThrow, Pure, Idempotent {
  /// Target type for the type cast.
  final CType testedType;

  /// Whether this type cast involves check at runtime.
  bool isChecked;

  TypeCast(
    FlowGraph graph,
    SourcePosition sourcePosition,
    Definition object,
    this.testedType,
    Definition? typeParameters, {
    this.isChecked = true,
  }) : super(graph, sourcePosition, typeParameters != null ? 2 : 1) {
    setInputAt(0, object);
    if (typeParameters != null) {
      setInputAt(1, typeParameters);
    }
  }

  Definition get operand => inputDefAt(0);
  Definition? get typeParameters => (inputCount > 1) ? inputDefAt(1) : null;

  @override
  CType get type => testedType;

  @override
  bool get canThrow => isChecked;

  @override
  bool attributesEqual(covariant TypeCast other) =>
      // 'isChecked' is not taken into account as checked and unchecked casts
      // against the same type are congruent wrt value numbering.
      testedType == other.testedType;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitTypeCast(this);
}

/// Test if input object is assignable to the given type.
final class TypeTest extends Definition with NoThrow, Pure, Idempotent {
  final CType testedType;

  TypeTest(
    FlowGraph graph,
    SourcePosition sourcePosition,
    Definition object,
    this.testedType,
    Definition? typeParameters,
  ) : super(graph, sourcePosition, typeParameters != null ? 2 : 1) {
    setInputAt(0, object);
    if (typeParameters != null) {
      setInputAt(1, typeParameters);
    }
  }

  Definition get operand => inputDefAt(0);
  Definition? get typeParameters => (inputCount > 1) ? inputDefAt(1) : null;

  @override
  CType get type => const BoolType();

  @override
  bool attributesEqual(covariant TypeTest other) =>
      testedType == other.testedType;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitTypeTest(this);
}

/// Represents a list of type arguments passed to a call.
///
/// Only used as the first input of call instructions.
final class TypeArguments extends Definition with NoThrow, Pure, Idempotent {
  final List<ast.DartType> types;
  TypeArguments(
    FlowGraph graph,
    SourcePosition sourcePosition,
    this.types,
    Definition? typeParameters,
  ) : super(graph, sourcePosition, typeParameters != null ? 1 : 0) {
    if (typeParameters != null) {
      setInputAt(0, typeParameters);
    }
  }

  Definition? get typeParameters => (inputCount > 0) ? inputDefAt(0) : null;

  @override
  CType get type => const TypeArgumentsType();

  @override
  bool attributesEqual(covariant TypeArguments other) =>
      listEquals(types, other.types);

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitTypeArguments(this);
}

enum BinaryIntOpcode {
  add('+'),
  sub('-'),
  mul('*'),
  truncatingDiv('~/'),
  mod('%'),
  rem('remainder'),
  bitOr('|'),
  bitAnd('&'),
  bitXor('^'),
  shiftLeft('<<'),
  shiftRight('>>'),
  unsignedShiftRight('>>>');

  final String token;
  const BinaryIntOpcode(this.token);

  bool get isCommutative => switch (this) {
    add || mul || bitOr || bitAnd || bitXor => true,
    _ => false,
  };
}

/// Binary operation on two int operands.
final class BinaryIntOp extends Definition with Pure, Idempotent {
  BinaryIntOpcode op;

  BinaryIntOp(
    FlowGraph graph,
    SourcePosition sourcePosition,
    this.op,
    Definition left,
    Definition right,
  ) : super(graph, sourcePosition, 2) {
    setInputAt(0, left);
    setInputAt(1, right);
  }

  Definition get left => inputDefAt(0);
  Definition get right => inputDefAt(1);

  @override
  bool get canThrow => switch (op) {
    BinaryIntOpcode.truncatingDiv ||
    BinaryIntOpcode.mod ||
    BinaryIntOpcode.rem => right.canBeZero,
    BinaryIntOpcode.shiftLeft ||
    BinaryIntOpcode.shiftRight ||
    BinaryIntOpcode.unsignedShiftRight => right.canBeNegative,
    _ => false,
  };

  @override
  CType get type => const IntType();

  @override
  bool attributesEqual(covariant BinaryIntOp other) => op == other.op;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitBinaryIntOp(this);
}

enum UnaryIntOpcode {
  neg('-'),
  bitNot('~'),
  toDouble('toDouble'),
  abs('abs'),
  sign('sign');

  final String token;
  const UnaryIntOpcode(this.token);
}

/// Unary operation on the int operand.
final class UnaryIntOp extends Definition with NoThrow, Pure, Idempotent {
  UnaryIntOpcode op;

  UnaryIntOp(
    FlowGraph graph,
    SourcePosition sourcePosition,
    this.op,
    Definition operand,
  ) : super(graph, sourcePosition, 1) {
    setInputAt(0, operand);
  }

  Definition get operand => inputDefAt(0);

  @override
  CType get type => switch (op) {
    UnaryIntOpcode.toDouble => const DoubleType(),
    _ => const IntType(),
  };

  @override
  bool attributesEqual(covariant UnaryIntOp other) => op == other.op;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitUnaryIntOp(this);
}

enum BinaryDoubleOpcode {
  add('+'),
  sub('-'),
  mul('*'),
  div('/'),
  truncatingDiv('~/'),
  mod('%'),
  rem('remainder');

  final String token;
  const BinaryDoubleOpcode(this.token);

  bool get isCommutative => switch (this) {
    add || mul => true,
    _ => false,
  };
}

/// Binary operation on two double operands.
final class BinaryDoubleOp extends Definition with NoThrow, Pure, Idempotent {
  BinaryDoubleOpcode op;

  BinaryDoubleOp(
    FlowGraph graph,
    SourcePosition sourcePosition,
    this.op,
    Definition left,
    Definition right,
  ) : super(graph, sourcePosition, 2) {
    setInputAt(0, left);
    setInputAt(1, right);
  }

  Definition get left => inputDefAt(0);
  Definition get right => inputDefAt(1);

  @override
  CType get type => switch (op) {
    BinaryDoubleOpcode.truncatingDiv => const IntType(),
    _ => const DoubleType(),
  };

  @override
  bool attributesEqual(covariant BinaryDoubleOp other) => op == other.op;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitBinaryDoubleOp(this);
}

enum UnaryDoubleOpcode {
  neg('-'),
  abs('abs'),
  sign('sign'),
  square('square'),
  round('round'),
  floor('floor'),
  ceil('ceil'),
  truncate('truncate'),
  roundToDouble('roundToDouble'),
  floorToDouble('floorToDouble'),
  ceilToDouble('ceilToDouble'),
  truncateToDouble('truncateToDouble');

  final String token;
  const UnaryDoubleOpcode(this.token);
}

/// Unary operation on the double operand.
final class UnaryDoubleOp extends Definition with NoThrow, Pure, Idempotent {
  UnaryDoubleOpcode op;

  UnaryDoubleOp(
    FlowGraph graph,
    SourcePosition sourcePosition,
    this.op,
    Definition operand,
  ) : super(graph, sourcePosition, 1) {
    setInputAt(0, operand);
  }

  Definition get operand => inputDefAt(0);

  @override
  CType get type => switch (op) {
    UnaryDoubleOpcode.round ||
    UnaryDoubleOpcode.floor ||
    UnaryDoubleOpcode.ceil ||
    UnaryDoubleOpcode.truncate => const IntType(),
    _ => const DoubleType(),
  };

  @override
  bool attributesEqual(covariant UnaryDoubleOp other) => op == other.op;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitUnaryDoubleOp(this);
}

/// Marker for the back-end specific instructions.
base mixin BackendInstruction on Instruction {}

/// Combined comparison and branch.
///
/// Certain back-ends may create [CompareAndBranch] during lowering.
final class CompareAndBranch extends Instruction
    with NoThrow, Pure, BackendInstruction
    implements ControlFlowInstruction {
  ComparisonOpcode op;

  CompareAndBranch(
    FlowGraph graph,
    SourcePosition sourcePosition,
    this.op,
    Definition left,
    Definition right,
  ) : super(graph, sourcePosition, 2) {
    setInputAt(0, left);
    setInputAt(1, right);
  }

  Definition get left => inputDefAt(0);
  Definition get right => inputDefAt(1);
  TargetBlock get trueSuccessor => block!.successors[0] as TargetBlock;
  TargetBlock get falseSuccessor => block!.successors[1] as TargetBlock;

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitCompareAndBranch(this);
}

/// Base class for move operations, part of [ParallelMove].
abstract base class MoveOp {}

/// In native back-ends, register allocator inserts [ParallelMove]
/// instructions to copy values atomically between registers
/// and memory locations.
final class ParallelMove extends Instruction
    with NoThrow, HasSideEffects, BackendInstruction {
  final List<MoveOp> moves = [];

  ParallelMove(FlowGraph graph) : super(graph, noPosition, 0);

  @override
  R accept<R>(InstructionVisitor<R> v) => v.visitParallelMove(this);
}
